/*
 * Copyright (c) 2001 Sun Microsystems, Inc.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *       Sun Microsystems, Inc. for Project JXTA."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact Project JXTA at http://www.jxta.org.
 *
 * 5. Products derived from this software may not be called "JXTA",
 *    nor may "JXTA" appear in their name, without prior written
 *    permission of Sun.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 *====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of Project JXTA.  For more
 * information on Project JXTA, please see
 * <http://www.jxta.org/>.
 *
 * This license is based on the BSD license adopted by the Apache Foundation.
 *
 * $Id: MimeTable.java,v 1.6 2004/06/09 22:44:08 gonzo Exp $
 *
 */

package net.jxta.share;

import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Hashtable;
import java.util.Enumeration;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import org.apache.log4j.Logger;

/**
 * This class is used to map file extensions to their corresponding
 * MimeInfo objects.
 */
public class MimeTable {
    private Hashtable tmap; // maps major type to MimeInfo object
    private Hashtable emap; // maps file extension to MimeInfo object
    private String browser; // browser application command

    private static final Logger LOG = Logger.getLogger(MimeTable.class);

    // default mime table
    private static final String MIME_PROPERTIES = "mime.properties";
    private static MimeTable defaultMimeTable;
    static {
	defaultMimeTable = new MimeTable();
	InputStream is = MimeTable.class.getResourceAsStream(MIME_PROPERTIES);

	if (is == null) {
            LOG.error("Cannot load default mime table. " + MIME_PROPERTIES +
                       " file not found.");
	    
	    throw new InternalError("Could not find " + MIME_PROPERTIES);
	} else {
	    try {
	        defaultMimeTable.load(is);
	    } catch (IOException e) {
	        e.printStackTrace();
	        throw new InternalError("Could not load default mime table");
	    }
	}
    }

    /**
     * Returns default, platform-independent MimeTable.
     */
    public static MimeTable getDefaultMimeTable() {
	return defaultMimeTable;
    }
    
    /**
     * Creates a new empty MimeTable.
     */
    public MimeTable() {
	tmap = new Hashtable();
	emap = new Hashtable();
    }

    /**
     * Returns the MimeInfo object for the specified mime type, or null if not
     * found.
     */
    public MimeInfo get(String type) {
	return (MimeInfo)tmap.get(type);
    }

    /**
     * Returns an array of all the MimeInfo objects in this table.
     */
    public MimeInfo[] getAll() {
	synchronized (this) {
	    MimeInfo[] mis = new MimeInfo[tmap.size()];
	    Enumeration e = tmap.elements();
	    for (int i = 0; i < mis.length; i++) {
		mis[i] = (MimeInfo)e.nextElement();
	    }
	    return mis;
	}
    }

    /**
     * Returns true if this mime table contains information for the
     * specific mime type.
     */
    public boolean contains(String type) {
	synchronized (this) {
	    return tmap.containsKey(type);
	}
    }

    /**
     * Adds a new MimeInfo object to the table, replacing any previous mapping
     * for the mime type.
     */
    public void put(MimeInfo mi) {
	String type = mi.getType();
	synchronized (this) {
	    if (tmap.containsKey(type)) {
		remove(type);
	    }
	    tmap.put(type, mi);
	    // create mappings for mime type file extensions
	    String[] exts = mi.getFileExtensions();
	    if (exts != null) {
		for (int i = 0; i < exts.length; i++) {
		    emap.put(exts[i], mi);
		}
	    }
	}
    }

    /**
     * Removes MimeInfo object corresponding to specified mime type.
     * @throw IllegalArgumentException if mime type not found
     */
    public void remove(String type) {
	synchronized (this) {
	    MimeInfo mi = (MimeInfo)tmap.remove(type);
	    if (mi == null) {
		throw new IllegalArgumentException(
		    "Mime type not found in table: " + mi.getType());
	    }
	    // remove mappings for the mime type file extensions
	    String[] exts = mi.getFileExtensions();
	    if (exts != null) {
		for (int i = 0; i < exts.length; i++) {
		    String ext = exts[i];
		    if (emap.get(ext) == mi) {
			emap.remove(ext);
		    }
		}
	    }
	}
    }

    /**
     * Returns the number of MimeInfo objects stored in this table.
     */
    public int size() {
	synchronized (this) {
	    return tmap.size();
	}
    }

    /**
     * Returns the MimeInfo for the specified file name, or null if not found.
     */
    public MimeInfo getForName(String name) {
	int i = name.indexOf('.');
	synchronized (this) {
	    return i != -1 ? (MimeInfo)emap.get(name.substring(i)) : null;
	}
    }

    /**
     * Loads mime types from the specified input stream.
     */
    public void load(InputStream is) throws IOException {
	Properties props = new Properties();
	props.load(is);
	browser = props.getProperty("browser.application");
	// parse mime type properties
	Enumeration e = props.keys();
	while (e.hasMoreElements()) {
	    String s = (String)e.nextElement();
	    if (s.indexOf('/') != -1) {
		put(parseProperty(s, props.getProperty(s)));
	    }
	}
    }

    // parse mime type information from the specified property value
    private MimeInfo parseProperty(String type, String value) {
	MimeInfo mi = new MimeInfo(type);
	StringTokenizer st = new StringTokenizer(value, ";");
	while (st.hasMoreTokens()) {
	    String s = st.nextToken();
	    parseKeyword(s, mi);
        }
	return mi;
    }

    // parse keyword and value from property value
    private void parseKeyword(String s, MimeInfo mi) {
	int i = s.indexOf('=');
	if (i == -1) {
	    throw new IllegalArgumentException("Invalid keyword: " + s);
	}
	String name = s.substring(0, i);
	String value = s.substring(i + 1);
	if (value.length() > 0) {
	    if (name.equals("description")) {
		mi.setDescription(value);
	    } else if (name.equals("file_extensions")) {
		mi.setFileExtensions(parseList(value));
	    } else if (name.equals("icon")) {
		mi.setIcon(value);
	    } else if (name.equals("action")) {
		mi.setAction(value);
	    } else if (name.equals("application")) {
		mi.setApplication(value);
	    } else {
		// ignore unrecognized keywords
	    }
	}
    }

    // parse comma separated list of strings
    private String[] parseList(String s) {
	StringTokenizer st = new StringTokenizer(s, ",");
	String[] strs = new String[st.countTokens()];
	for (int i = 0; i < strs.length; i++) {
	    strs[i] = st.nextToken();
	}
	return strs;
    }

    /**
     * Saves this MimeTable to the specified output stream.
     */
    public void save(OutputStream os) throws IOException {
	Properties props = new Properties();
	synchronized (this) {
	    if (browser != null) {
		props.put("browser.application", browser);
	    }
	    StringBuffer sb = new StringBuffer();
	    Enumeration e = tmap.keys();
	    while (e.hasMoreElements()) {
		String type = (String)e.nextElement();
		MimeInfo mi = (MimeInfo)tmap.get(type);
		sb.append("description=");
		if (mi.getDescription() != null) {
		    sb.append(mi.getDescription());
		}
		if (mi.getFileExtensions() != null) {
		    sb.append(";file_extensions=");
		    String[] exts = mi.getFileExtensions();
		    if (exts.length > 0) {
			sb.append(exts[0]);
			for (int i = 1; i < exts.length; i++) {
			    sb.append(',');
			    sb.append(exts[i]);
			}
		    }
		}
		if (mi.getIcon() != null) {
		    sb.append(";icon=");
		    sb.append(mi.getIcon());
		}
		if (mi.getAction() != null) {
		    sb.append(";action=");
		    sb.append(mi.getAction());
		}
		if (mi.getApplication() != null) {
		    sb.append(";application=");
		    sb.append(mi.getApplication());
		}
		props.put(type, sb.toString());
		sb.setLength(0);
	    }
	}
	props.store(os, "Mime properties");
    }

    /**
     * Returns the browser application launch string or null if none.
     */
    public String getBrowserApplication() {
	synchronized (this) {
	    return browser;
	}
    }

    /**
     * Sets the browser application launch string.
     */
    public void setBrowserApplication(String browser) {
	synchronized (this) {
	    this.browser = browser;
	}
    }

    /**
     * Returns an array of strings that can be used to launch
     * the browser for the specified argument using Runtime.exec().
     */
    public String[] getBrowserCmdArray(String arg) {
	synchronized (this) {
	    if (browser != null) {
		return getCommandArray(browser, arg);
	    }
	}
	return null;
    }

    /**
     * Parses the specified command string into an array of strings
     * appropriate for use with Runtime.exec(). Arguments containing
     * single and double quotes are properly handled. The special
     * token '%s' if not contained in quotes will be substituted with
     * the specified optional argument if not null. 
     */
    public static String[] getCommandArray(String cmd, String arg) {
	Vector v = new Vector();
	StringBuffer sb = new StringBuffer();
	boolean inToken = false;
	boolean quoted = false;
	char quoteChar = 0;
	char[] cs = new char[cmd.length()];
	cmd.getChars(0, cs.length, cs, 0);
	for (int i = 0; i < cs.length; i++) {
	    char c = cs[i];
	    switch (c) {
	    case ' ': case '\t': case '\n': case '\r': case '\f':
		if (inToken) {
		    if (quoted) {
			sb.append(c);
		    } else {
			v.addElement(sb.toString());
			sb.setLength(0);
			inToken = false;
		    }
		}
		continue;
	    case '\'': case '"':
		if (inToken) {
		    if (quoted) {
			if (c == quoteChar) {
			    quoted = false;
			} else {
			    sb.append(c);
			}
		    } else {
			quoted = true;
			quoteChar = c;
		    }
		} else {
		    inToken = quoted = true;
		    quoteChar = c;
		}
		continue;
	    case '%':
		if (!inToken) {
		    int j = i + 1;
		    if (j < cs.length && cs[j++] == 's') {
			if (j >= cs.length || isSpaceChar(cs[j])) {
			    if (arg != null) {
				v.addElement(arg);
			    }
			    i++;
			    continue;
			}
		    }
		}
	    default:
		if (!inToken) {
		    inToken = true;
		    quoted = false;
		}
		sb.append(c);
		continue;
	    }
	}
	if (inToken) {
	    if (quoted) {
		throw new IllegalArgumentException(
		    "Unmatched quote in command string: " + cmd);
	    }
	    v.addElement(sb.toString());
	}
	String[] cmdArray = new String[v.size()];
	v.copyInto(cmdArray);
	return cmdArray;
    }

    private static boolean isSpaceChar(char c) {
	switch (c) {
	case ' ': case '\t': case '\n': case '\r': case '\f':
	    return true;
	default:
	    return false;
	}
    }
}
